/**
 * $RCSfile$
 * $Revision: $
 * $Date: $
 *
 * Copyright 2003-2006 Jive Software.
 *
 * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jivesoftware.smackx;

import org.jivesoftware.smack.Connection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.util.Cache;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smackx.packet.DiscoverInfo;
import org.jivesoftware.smackx.packet.DiscoverItems;
import org.jivesoftware.smackx.packet.MultipleAddresses;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * A MultipleRecipientManager allows to send packets to multiple recipients by
 * making use of <a href="http://www.jabber.org/jeps/jep-0033.html">JEP-33:
 * Extended Stanza Addressing</a>. It also allows to send replies to packets
 * that were sent to multiple recipients.
 * 
 * @author Gaston Dombiak
 */
public class MultipleRecipientManager {

	/**
	 * Create a cache to hold the 100 most recently accessed elements for a
	 * period of 24 hours.
	 */
	private static Cache services = new Cache(100, 24 * 60 * 60 * 1000);

	/**
	 * Sends the specified packet to the list of specified recipients using the
	 * specified connection. If the server has support for JEP-33 then only one
	 * packet is going to be sent to the server with the multiple recipient
	 * instructions. However, if JEP-33 is not supported by the server then the
	 * client is going to send the packet to each recipient.
	 * 
	 * @param connection
	 *            the connection to use to send the packet.
	 * @param packet
	 *            the packet to send to the list of recipients.
	 * @param to
	 *            the list of JIDs to include in the TO list or <tt>null</tt> if
	 *            no TO list exists.
	 * @param cc
	 *            the list of JIDs to include in the CC list or <tt>null</tt> if
	 *            no CC list exists.
	 * @param bcc
	 *            the list of JIDs to include in the BCC list or <tt>null</tt>
	 *            if no BCC list exists.
	 * @throws XMPPException
	 *             if server does not support JEP-33: Extended Stanza Addressing
	 *             and some JEP-33 specific features were requested.
	 */
	public static void send(Connection connection, Packet packet, List to,
			List cc, List bcc) throws XMPPException {
		send(connection, packet, to, cc, bcc, null, null, false);
	}

	/**
	 * Sends the specified packet to the list of specified recipients using the
	 * specified connection. If the server has support for JEP-33 then only one
	 * packet is going to be sent to the server with the multiple recipient
	 * instructions. However, if JEP-33 is not supported by the server then the
	 * client is going to send the packet to each recipient.
	 * 
	 * @param connection
	 *            the connection to use to send the packet.
	 * @param packet
	 *            the packet to send to the list of recipients.
	 * @param to
	 *            the list of JIDs to include in the TO list or <tt>null</tt> if
	 *            no TO list exists.
	 * @param cc
	 *            the list of JIDs to include in the CC list or <tt>null</tt> if
	 *            no CC list exists.
	 * @param bcc
	 *            the list of JIDs to include in the BCC list or <tt>null</tt>
	 *            if no BCC list exists.
	 * @param replyTo
	 *            address to which all replies are requested to be sent or
	 *            <tt>null</tt> indicating that they can reply to any address.
	 * @param replyRoom
	 *            JID of a MUC room to which responses should be sent or
	 *            <tt>null</tt> indicating that they can reply to any address.
	 * @param noReply
	 *            true means that receivers should not reply to the message.
	 * @throws XMPPException
	 *             if server does not support JEP-33: Extended Stanza Addressing
	 *             and some JEP-33 specific features were requested.
	 */
	public static void send(Connection connection, Packet packet, List to,
			List cc, List bcc, String replyTo, String replyRoom, boolean noReply)
			throws XMPPException {
		String serviceAddress = getMultipleRecipienServiceAddress(connection);
		if (serviceAddress != null) {
			// Send packet to target users using multiple recipient service
			// provided by the server
			sendThroughService(connection, packet, to, cc, bcc, replyTo,
					replyRoom, noReply, serviceAddress);
		} else {
			// Server does not support JEP-33 so try to send the packet to each
			// recipient
			if (noReply || (replyTo != null && replyTo.trim().length() > 0)
					|| (replyRoom != null && replyRoom.trim().length() > 0)) {
				// Some specified JEP-33 features were requested so throw an
				// exception alerting
				// the user that this features are not available
				throw new XMPPException(
						"Extended Stanza Addressing not supported by server");
			}
			// Send the packet to each individual recipient
			sendToIndividualRecipients(connection, packet, to, cc, bcc);
		}
	}

	/**
	 * Sends a reply to a previously received packet that was sent to multiple
	 * recipients. Before attempting to send the reply message some checkings
	 * are performed. If any of those checkings fail then an XMPPException is
	 * going to be thrown with the specific error detail.
	 * 
	 * @param connection
	 *            the connection to use to send the reply.
	 * @param original
	 *            the previously received packet that was sent to multiple
	 *            recipients.
	 * @param reply
	 *            the new message to send as a reply.
	 * @throws XMPPException
	 *             if the original message was not sent to multiple recipients,
	 *             or the original message cannot be replied or reply should be
	 *             sent to a room.
	 */
	public static void reply(Connection connection, Message original,
			Message reply) throws XMPPException {
		MultipleRecipientInfo info = getMultipleRecipientInfo(original);
		if (info == null) {
			throw new XMPPException(
					"Original message does not contain multiple recipient info");
		}
		if (info.shouldNotReply()) {
			throw new XMPPException("Original message should not be replied");
		}
		if (info.getReplyRoom() != null) {
			throw new XMPPException("Reply should be sent through a room");
		}
		// Any <thread/> element from the initial message MUST be copied into
		// the reply.
		if (original.getThread() != null) {
			reply.setThread(original.getThread());
		}
		MultipleAddresses.Address replyAddress = info.getReplyAddress();
		if (replyAddress != null && replyAddress.getJid() != null) {
			// Send reply to the reply_to address
			reply.setTo(replyAddress.getJid());
			connection.sendPacket(reply);
		} else {
			// Send reply to multiple recipients
			List to = new ArrayList();
			List cc = new ArrayList();
			for (Iterator it = info.getTOAddresses().iterator(); it.hasNext();) {
				String jid = ((MultipleAddresses.Address) it.next()).getJid();
				to.add(jid);
			}
			for (Iterator it = info.getCCAddresses().iterator(); it.hasNext();) {
				String jid = ((MultipleAddresses.Address) it.next()).getJid();
				cc.add(jid);
			}
			// Add original sender as a 'to' address (if not already present)
			if (!to.contains(original.getFrom())
					&& !cc.contains(original.getFrom())) {
				to.add(original.getFrom());
			}
			// Remove the sender from the TO/CC list (try with bare JID too)
			String from = connection.getUser();
			if (!to.remove(from) && !cc.remove(from)) {
				String bareJID = StringUtils.parseBareAddress(from);
				to.remove(bareJID);
				cc.remove(bareJID);
			}

			String serviceAddress = getMultipleRecipienServiceAddress(connection);
			if (serviceAddress != null) {
				// Send packet to target users using multiple recipient service
				// provided by the server
				sendThroughService(connection, reply, to, cc, null, null, null,
						false, serviceAddress);
			} else {
				// Server does not support JEP-33 so try to send the packet to
				// each recipient
				sendToIndividualRecipients(connection, reply, to, cc, null);
			}
		}
	}

	/**
	 * Returns the {@link MultipleRecipientInfo} contained in the specified
	 * packet or <tt>null</tt> if none was found. Only packets sent to multiple
	 * recipients will contain such information.
	 * 
	 * @param packet
	 *            the packet to check.
	 * @return the MultipleRecipientInfo contained in the specified packet or
	 *         <tt>null</tt> if none was found.
	 */
	public static MultipleRecipientInfo getMultipleRecipientInfo(Packet packet) {
		MultipleAddresses extension = (MultipleAddresses) packet.getExtension(
				"addresses", "http://jabber.org/protocol/address");
		return extension == null ? null : new MultipleRecipientInfo(extension);
	}

	private static void sendToIndividualRecipients(Connection connection,
			Packet packet, List to, List cc, List bcc) {
		if (to != null) {
			for (Iterator it = to.iterator(); it.hasNext();) {
				String jid = (String) it.next();
				packet.setTo(jid);
				connection.sendPacket(new PacketCopy(packet.toXML()));
			}
		}
		if (cc != null) {
			for (Iterator it = cc.iterator(); it.hasNext();) {
				String jid = (String) it.next();
				packet.setTo(jid);
				connection.sendPacket(new PacketCopy(packet.toXML()));
			}
		}
		if (bcc != null) {
			for (Iterator it = bcc.iterator(); it.hasNext();) {
				String jid = (String) it.next();
				packet.setTo(jid);
				connection.sendPacket(new PacketCopy(packet.toXML()));
			}
		}
	}

	private static void sendThroughService(Connection connection,
			Packet packet, List to, List cc, List bcc, String replyTo,
			String replyRoom, boolean noReply, String serviceAddress) {
		// Create multiple recipient extension
		MultipleAddresses multipleAddresses = new MultipleAddresses();
		if (to != null) {
			for (Iterator it = to.iterator(); it.hasNext();) {
				String jid = (String) it.next();
				multipleAddresses.addAddress(MultipleAddresses.TO, jid, null,
						null, false, null);
			}
		}
		if (cc != null) {
			for (Iterator it = cc.iterator(); it.hasNext();) {
				String jid = (String) it.next();
				multipleAddresses.addAddress(MultipleAddresses.CC, jid, null,
						null, false, null);
			}
		}
		if (bcc != null) {
			for (Iterator it = bcc.iterator(); it.hasNext();) {
				String jid = (String) it.next();
				multipleAddresses.addAddress(MultipleAddresses.BCC, jid, null,
						null, false, null);
			}
		}
		if (noReply) {
			multipleAddresses.setNoReply();
		} else {
			if (replyTo != null && replyTo.trim().length() > 0) {
				multipleAddresses.addAddress(MultipleAddresses.REPLY_TO,
						replyTo, null, null, false, null);
			}
			if (replyRoom != null && replyRoom.trim().length() > 0) {
				multipleAddresses.addAddress(MultipleAddresses.REPLY_ROOM,
						replyRoom, null, null, false, null);
			}
		}
		// Set the multiple recipient service address as the target address
		packet.setTo(serviceAddress);
		// Add extension to packet
		packet.addExtension(multipleAddresses);
		// Send the packet
		connection.sendPacket(packet);
	}

	/**
	 * Returns the address of the multiple recipients service. To obtain such
	 * address service discovery is going to be used on the connected server and
	 * if none was found then another attempt will be tried on the server items.
	 * The discovered information is going to be cached for 24 hours.
	 * 
	 * @param connection
	 *            the connection to use for disco. The connected server is going
	 *            to be queried.
	 * @return the address of the multiple recipients service or <tt>null</tt>
	 *         if none was found.
	 */
	private static String getMultipleRecipienServiceAddress(
			Connection connection) {
		String serviceName = connection.getServiceName();
		String serviceAddress = (String) services.get(serviceName);
		if (serviceAddress == null) {
			synchronized (services) {
				serviceAddress = (String) services.get(serviceName);
				if (serviceAddress == null) {

					// Send the disco packet to the server itself
					try {
						DiscoverInfo info = ServiceDiscoveryManager
								.getInstanceFor(connection).discoverInfo(
										serviceName);
						// Check if the server supports JEP-33
						if (info.containsFeature("http://jabber.org/protocol/address")) {
							serviceAddress = serviceName;
						} else {
							// Get the disco items and send the disco packet to
							// each server item
							DiscoverItems items = ServiceDiscoveryManager
									.getInstanceFor(connection).discoverItems(
											serviceName);
							for (Iterator it = items.getItems(); it.hasNext();) {
								DiscoverItems.Item item = (DiscoverItems.Item) it
										.next();
								info = ServiceDiscoveryManager.getInstanceFor(
										connection).discoverInfo(
										item.getEntityID(), item.getNode());
								if (info.containsFeature("http://jabber.org/protocol/address")) {
									serviceAddress = serviceName;
									break;
								}
							}

						}
						// Cache the discovered information
						services.put(serviceName, serviceAddress == null ? ""
								: serviceAddress);
					} catch (XMPPException e) {
						e.printStackTrace();
					}
				}
			}
		}

		return "".equals(serviceAddress) ? null : serviceAddress;
	}

	/**
	 * Packet that holds the XML stanza to send. This class is useful when the
	 * same packet is needed to be sent to different recipients. Since using the
	 * same packet is not possible (i.e. cannot change the TO address of a
	 * queues packet to be sent) then this class was created to keep the XML
	 * stanza to send.
	 */
	private static class PacketCopy extends Packet {

		private String text;

		/**
		 * Create a copy of a packet with the text to send. The passed text must
		 * be a valid text to send to the server, no validation will be done on
		 * the passed text.
		 * 
		 * @param text
		 *            the whole text of the packet to send
		 */
		public PacketCopy(String text) {
			this.text = text;
		}

		public String toXML() {
			return text;
		}

	}

}
